﻿'  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
'  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
'  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
'  PURPOSE.
'
'  License: GNU Lesser General Public License (LGPLv3)
'
'  Email: p_torgashov@ukr.net.
'
'  Copyright (C) Pavel Torgashov, 2012-2015. 

Imports System
Imports System.Drawing
Imports System.Collections
Imports System.Windows.Forms
Imports System.ComponentModel
Imports System.Collections.Generic
Imports System.Text.RegularExpressions
Imports AutoCompleteMenuNS

<ProvideProperty("AutoCompleteMenu", GetType(Control))>
Public Class AutoCompleteMenu
    Inherits Component
    Implements IExtenderProvider

    Private Shared ReadOnly AutoCompleteMenuByControls As New Dictionary(Of Control, AutoCompleteMenu)()
    Private Shared ReadOnly WrapperByControls As New Dictionary(Of Control, ITextBoxWrapper)()
    Private sourceItems As IEnumerable(Of AutoCompleteItem) = New List(Of AutoCompleteItem)()

    Private _targetControlWrapper As ITextBoxWrapper
    Private ReadOnly timer As New Timer()
    Private _maximumSize As Size
    Private _fragment As Range

    <Browsable(False)>
    Public Property VisibleItems() As IList(Of AutoCompleteItem)
        Get
            Return Host.ListView.VisibleItems
        End Get
        Private Set(value As IList(Of AutoCompleteItem))
            Host.ListView.VisibleItems = value
        End Set
    End Property

    ''' <summary>
    ''' Duration (ms) of tooltip showing
    ''' </summary>
    <Description("Duration (ms) of tooltip showing")>
    <DefaultValue(3000)>
    Public Property ToolTipDuration() As Integer
        Get
            Return Host.ListView.ToolTipDuration
        End Get
        Set(value As Integer)
            Host.ListView.ToolTipDuration = value
        End Set
    End Property

    <Description("Display tooltip direction")>
    Public Property ToolTipRightToLeft As RightToLeft
        Get
            Return Host.ListView.ToolTipRightToLeft
        End Get
        Set(value As RightToLeft)
            Host.ListView.ToolTipRightToLeft = value
        End Set
    End Property

    <Description("Display tooltip font")>
    Public Property ToolTipFont As Font
        Get
            Return Host.ListView.ToolTipFont
        End Get
        Set(value As Font)
            Host.ListView.ToolTipFont = value
        End Set
    End Property

    <Description("Display tooltip title font")>
    Public Property ToolTipTitleFont As Font
        Get
            Return Host.ListView.ToolTipTitleFont
        End Get
        Set(value As Font)
            Host.ListView.ToolTipTitleFont = value
        End Set
    End Property

    Public Sub New()
        Host = New AutoCompleteMenuHost(Me)
        AddHandler Host.ListView.ItemSelected, New EventHandler(AddressOf ListView_ItemSelected)
        AddHandler Host.ListView.ItemHovered, New EventHandler(Of HoveredEventArgs)(AddressOf ListView_ItemHovered)
        VisibleItems = New List(Of AutoCompleteItem)()
        Enabled = True
        AppearInterval = 500
        AddHandler timer.Tick, AddressOf Timer_Tick
        MaximumSize = New Size(180, 200)
        MaxItemCount = 50
        AutoPopup = True

        SearchPattern = "[\w]"
        MinFragmentLength = 2
    End Sub
    Protected Overrides Sub Dispose(disposing As Boolean)
        If disposing Then
            timer.Dispose()
            Host.Dispose()
        End If
        MyBase.Dispose(disposing)
    End Sub

    Sub ListView_ItemSelected(sender As Object, e As EventArgs)
        OnSelecting()
    End Sub

    Sub ListView_ItemHovered(sender As Object, e As HoveredEventArgs)
        OnHovered(e)
    End Sub

    Public Sub OnHovered(e As HoveredEventArgs)
        RaiseEvent Hovered(Me, e)
    End Sub

    <Browsable(False)>
    Public Property SelectedItemIndex() As Integer
        Get
            Return Host.ListView.SelectedItemIndex
        End Get
        Friend Set(value As Integer)
            Host.ListView.SelectedItemIndex = value
        End Set
    End Property

    Friend Property Host() As AutoCompleteMenuHost

    ''' <summary>
    ''' Called when user selected the control and needed wrapper over it.
    ''' You can assign own Wrapper for target control.
    ''' </summary>
    <Description("Called when user selected the control and needed wrapper over it. You can assign own Wrapper for target control.")>
    Public Event WrapperNeeded As EventHandler(Of WrapperNeededEventArgs)

    Protected Sub OnWrapperNeeded(args As WrapperNeededEventArgs)
        RaiseEvent WrapperNeeded(Me, args)
        If args.Wrapper Is Nothing Then
            args.Wrapper = TextBoxWrapper.Create(args.TargetControl)
        End If
    End Sub

    Function CreateWrapper(control As Control) As ITextBoxWrapper
        If WrapperByControls.ContainsKey(control) Then
            Return WrapperByControls(control)
        End If

        Dim args As New WrapperNeededEventArgs(control)
        OnWrapperNeeded(args)
        If args.Wrapper IsNot Nothing Then
            WrapperByControls(control) = args.Wrapper
        End If

        Return args.Wrapper
    End Function

    ''' <summary>
    ''' Current target control wrapper
    ''' </summary>
    <Browsable(False)>
    Public Property TargetControlWrapper() As ITextBoxWrapper
        Get
            Return _targetControlWrapper
        End Get
        Set(value As ITextBoxWrapper)
            _targetControlWrapper = value
            If value IsNot Nothing AndAlso Not WrapperByControls.ContainsKey(value.TargetControl) Then
                WrapperByControls(value.TargetControl) = value
                SetAutoCompleteMenu(value.TargetControl, Me)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Maximum size of popup menu
    ''' </summary>
    <DefaultValue(GetType(Size), "180, 200")>
    <Description("Maximum size of popup menu")>
    Public Property MaximumSize() As Size
        Get
            Return _maximumSize
        End Get
        Set(value As Size)
            _maximumSize = value
            CType(Host.ListView, Control).MaximumSize = _maximumSize
            CType(Host.ListView, Control).Size = _maximumSize
            Host.CalcSize()
        End Set
    End Property

    ''' <summary>
    ''' Font
    ''' </summary>
    Public Property Font() As Font
        Get
            Return CType(Host.ListView, Control).Font
        End Get
        Set(value As Font)
            CType(Host.ListView, Control).Font = value
        End Set
    End Property

    ''' <summary>
    ''' Left padding of text
    ''' </summary>
    <DefaultValue(18)>
    <Description("Left padding of text")>
    Public Property LeftPadding() As Integer
        Get
            If TypeOf Host.ListView Is AutoCompleteListView Then
                Return CType(Host.ListView, AutoCompleteListView).LeftPadding
            Else
                Return 0
            End If
        End Get
        Set(value As Integer)
            If TypeOf Host.ListView Is AutoCompleteListView Then
                CType(Host.ListView, AutoCompleteListView).LeftPadding = value
            End If
        End Set
    End Property

    ''' <summary>
    ''' Colors of foreground and background
    ''' </summary>
    <Browsable(True)>
    <Description("Colors of foreground and background.")>
    <TypeConverter(GetType(ExpandableObjectConverter))>
    Public Property Colors() As Colors
        Get
            Return CType(Host.ListView, IAutoCompleteListView).Colors
        End Get
        Set(value As Colors)
            CType(Host.ListView, IAutoCompleteListView).Colors = value
        End Set
    End Property

    ''' <summary>
    ''' AutoCompleteMenu will popup automatically (when user writes text). Otherwise it will popup only programmatically or by Ctrl-Space.
    ''' </summary>
    <DefaultValue(True)>
    <Description("AutoCompleteMenu will popup automatically (when user writes text). Otherwise it will popup only programmatically or by Ctrl-Space.")>
    Public Property AutoPopup() As Boolean

    ''' <summary>
    ''' AutoCompleteMenu will capture focus when opening.
    ''' </summary>
    <DefaultValue(False)>
    <Description("AutoCompleteMenu will capture focus when opening.")>
    Public Property CaptureFocus() As Boolean

    ''' <summary>
    ''' Indicates whether the component should draw right-to-left for RTL languages.
    ''' </summary>
    <Description("Indicates whether the component should draw right-to-left for RTL languages.")>
    Public Property RightToLeft() As RightToLeft
        Get
            Return Host.RightToLeft
        End Get
        Set(value As RightToLeft)
            Host.RightToLeft = value
        End Set
    End Property

    ''' <summary>
    ''' Image list
    ''' </summary>
    Public Property ImageList() As ImageList
        Get
            Return Host.ListView.ImageList
        End Get
        Set(value As ImageList)
            Host.ListView.ImageList = value
        End Set
    End Property

    ''' <summary>
    ''' Fragment
    ''' </summary>
    <Browsable(False)>
    Public Property Fragment() As Range
        Get
            Return _fragment
        End Get
        Friend Set(value As Range)
            _fragment = value
        End Set
    End Property

    ''' <summary>
    ''' Regex pattern for serach fragment around caret
    ''' </summary>
    <Description("Regex pattern for serach fragment around caret")>
    <DefaultValue("[\w]")>
    Public Property SearchPattern() As String

    ''' <summary>
    ''' Minimum fragment length for popup
    ''' </summary>
    <Description("Minimum fragment length for popup")>
    <DefaultValue(2)>
    Public Property MinFragmentLength() As Integer

    ''' <summary>
    ''' Allows TAB for select menu item
    ''' </summary>
    <Description("Allows TAB for select menu item")>
    <DefaultValue(False)>
    Public Property AllowsTabKey() As Boolean

    ''' <summary>
    ''' Interval of menu appear (ms)
    ''' </summary>
    <Description("Interval of menu appear (ms)")>
    <DefaultValue(500)>
    Public Property AppearInterval() As Integer

    <DefaultValue(GetType(String()), Nothing)>
    Public Property Items() As String()
        Get
            If sourceItems Is Nothing Then
                Return Nothing
            End If
            Dim list As New List(Of String)()
            For Each item As AutoCompleteItem In sourceItems
                list.Add(item.ToString())
            Next
            Return list.ToArray()
        End Get
        Set(value As String())
            SetAutoCompleteItems(value)
        End Set
    End Property

    ''' <summary>
    ''' The control for menu displaying.
    ''' Set to null for restore default ListView (AutoCompleteListView).
    ''' </summary>
    <Browsable(False)>
    Public Property ListView() As IAutoCompleteListView
        Get
            Return Host.ListView
        End Get
        Set(value As IAutoCompleteListView)
            If ListView IsNot Nothing Then
                Dim ctrl = CType(value, Control)
                value.ImageList = ImageList
                ctrl.RightToLeft = RightToLeft
                ctrl.Font = Font
                ctrl.MaximumSize = MaximumSize
            End If
            Host.ListView = value
            AddHandler Host.ListView.ItemSelected, New EventHandler(AddressOf ListView_ItemSelected)
            AddHandler Host.ListView.ItemHovered, New EventHandler(Of HoveredEventArgs)(AddressOf ListView_ItemHovered)
        End Set
    End Property

    <DefaultValue(True)>
    Public Property Enabled() As Boolean

    <Browsable(True)>
    <DefaultValue(50)>
    <Description("Maximum count of can appear item. will unlimited when small or equels then 0")>
    Public Property MaxItemCount As Integer

    ''' <summary>
    ''' Updates size of the menu
    ''' </summary>
    Public Sub Update()
        Host.CalcSize()
    End Sub

    ''' <summary>
    ''' Returns rectangle of item
    ''' </summary>
    Public Function GetItemRectangle(itemIndex As Integer) As Rectangle
        Return Host.ListView.GetItemRectangle(itemIndex)
    End Function

#Region "IExtenderProvider Members"

    Function CanExtend(extendee As Object) As Boolean Implements IExtenderProvider.CanExtend
        'find  AutoCompleteMenu with lowest hashcode
        If Container IsNot Nothing Then
            For Each comp As Object In Container.Components
                If TypeOf comp Is AutoCompleteMenu Then
                    If comp.GetHashCode() < GetHashCode() Then
                        Return False
                    End If
                End If
            Next
        End If
        'we are main autocomplete menu on form ...
        'check extendee as TextBox
        If Not (TypeOf extendee Is Control) Then
            Return False
        End If
        Dim temp = TextBoxWrapper.Create(CType(extendee, Control))
        Return temp IsNot Nothing
    End Function

    Public Sub SetAutoCompleteMenu(control As Control, menu As AutoCompleteMenu)
        If menu IsNot Nothing Then
            If WrapperByControls.ContainsKey(control) Then
                Return
            End If
            Dim wrapper = menu.CreateWrapper(control)
            If wrapper Is Nothing Then Return

            If control.IsHandleCreated Then
                menu.SubscribeForm(wrapper)
            Else
                AddHandler control.HandleCreated, Sub(o, e) menu.SubscribeForm(wrapper)
            End If

            AutoCompleteMenuByControls(control) = Me

            AddHandler wrapper.LostFocus, AddressOf menu.Control_LostFocus
            AddHandler wrapper.Scroll, AddressOf menu.Control_Scroll
            AddHandler wrapper.KeyDown, AddressOf menu.Control_KeyDown
            AddHandler wrapper.MouseDown, AddressOf menu.Control_MouseDown
        Else
            AutoCompleteMenuByControls.TryGetValue(control, menu)
            AutoCompleteMenuByControls.Remove(control)
            Dim wrapper As ITextBoxWrapper = Nothing
            WrapperByControls.TryGetValue(control, wrapper)
            WrapperByControls.Remove(control)
            If wrapper IsNot Nothing AndAlso menu IsNot Nothing Then
                RemoveHandler wrapper.LostFocus, AddressOf menu.Control_LostFocus
                RemoveHandler wrapper.Scroll, AddressOf menu.Control_Scroll
                RemoveHandler wrapper.KeyDown, AddressOf menu.Control_KeyDown
                RemoveHandler wrapper.MouseDown, AddressOf menu.Control_MouseDown
            End If
        End If
    End Sub

#End Region

    ''' <summary>
    ''' User selects item
    ''' </summary>
    <Description("Occurs when user selects item.")>
    Public Event Selecting As EventHandler(Of SelectingEventArgs)

    ''' <summary>
    ''' It fires after item was inserting
    ''' </summary>
    <Description("Occurs after user selected item.")>
    Public Event Selected As EventHandler(Of SelectedEventArgs)

    ''' <summary>
    ''' It fires when item was hovered
    ''' </summary>
    <Description("Occurs when user hovered item.")>
    Public Event Hovered As EventHandler(Of HoveredEventArgs)

    ''' <summary>
    ''' Occurs when popup menu is opening
    ''' </summary>
    Public Event Opening As EventHandler(Of CancelEventArgs)

    Private Sub Timer_Tick(sender As Object, e As EventArgs)
        timer.[Stop]()
        If TargetControlWrapper IsNot Nothing Then
            ShowAutoComplete(False)
        End If
    End Sub

    Private myForm As Form
    Sub SubscribeForm(wrapper As ITextBoxWrapper)
        If wrapper Is Nothing Then Return

        Dim form = wrapper.TargetControl.FindForm()
        If form Is Nothing Then Return

        If myForm IsNot Nothing Then
            If myForm Is form Then Return
            UnsubscribeForm(wrapper)
        End If

        myForm = form

        AddHandler form.LocationChanged, New EventHandler(AddressOf Form_LocationChanged)
        AddHandler form.ResizeBegin, New EventHandler(AddressOf Form_LocationChanged)
        AddHandler form.FormClosing, New FormClosingEventHandler(AddressOf Form_FormClosing)
        AddHandler form.LostFocus, New EventHandler(AddressOf Form_LocationChanged)
    End Sub
    Sub UnsubscribeForm(wrapper As ITextBoxWrapper)
        If wrapper Is Nothing Then Return

        Dim form = wrapper.TargetControl.FindForm()
        If form Is Nothing Then Return

        RemoveHandler form.LocationChanged, New EventHandler(AddressOf Form_LocationChanged)
        RemoveHandler form.ResizeBegin, New EventHandler(AddressOf Form_LocationChanged)
        RemoveHandler form.FormClosing, New FormClosingEventHandler(AddressOf Form_FormClosing)
        RemoveHandler form.LostFocus, New EventHandler(AddressOf Form_LocationChanged)
    End Sub

    Private Sub Form_FormClosing(sender As Object, e As FormClosingEventArgs)
        Close()
    End Sub

    Private Sub Form_LocationChanged(sender As Object, e As EventArgs)
        Close()
    End Sub

    Private Sub Control_MouseDown(sender As Object, e As MouseEventArgs)
        Close()
    End Sub

    Function FindWrapper(sender As Control) As ITextBoxWrapper
        While sender IsNot Nothing
            If WrapperByControls.ContainsKey(sender) Then
                Return WrapperByControls(sender)
            End If

            sender = sender.Parent
        End While

        Return Nothing
    End Function

    Private Sub Control_KeyDown(sender As Object, e As KeyEventArgs)
        TargetControlWrapper = FindWrapper(CType(sender, Control))
        Dim backspaceORdel As Boolean = e.KeyCode = Keys.Back OrElse e.KeyCode = Keys.Delete

        If Host.Visible Then
            If ProcessKey(e.KeyCode, Control.ModifierKeys) Then
                e.SuppressKeyPress = True
            ElseIf Not backspaceORdel Then
                ResetTimer(1)
            Else
                ResetTimer()
            End If
            Return
        End If

        If Not Host.Visible Then
            Select Case e.KeyCode
                Case Keys.Up, Keys.Down, Keys.PageUp, Keys.PageDown, Keys.Left, Keys.Right, Keys.[End], Keys.Home, Keys.ControlKey
                    timer.[Stop]()
                    Return
            End Select

            If Control.ModifierKeys = Keys.Control AndAlso e.KeyCode = Keys.Space Then
                ShowAutoComplete(True)
                e.SuppressKeyPress = True
                Return
            End If
        End If

        ResetTimer()
    End Sub

    Sub ResetTimer()
        ResetTimer(-1)
    End Sub

    Sub ResetTimer(interval As Integer)
        If interval <= 0 Then
            timer.Interval = AppearInterval
        Else
            timer.Interval = interval
        End If
        timer.[Stop]()
        timer.Start()
    End Sub

    Private Sub Control_Scroll(sender As Object, e As ScrollEventArgs)
        Close()
    End Sub

    Private Sub Control_LostFocus(sender As Object, e As EventArgs)
        If Not Host.Focused Then
            Close()
        End If
    End Sub

    Public Function GetAutoCompleteMenu(control As Control) As AutoCompleteMenu
        If AutoCompleteMenuByControls.ContainsKey(control) Then
            Return AutoCompleteMenuByControls(control)
        Else
            Return Nothing
        End If
    End Function

    Private forcedOpened As Boolean = False
    Friend Sub ShowAutoComplete(forced As Boolean)
        If forced Then forcedOpened = True

        If TargetControlWrapper IsNot Nothing AndAlso TargetControlWrapper.[ReadOnly] Then
            Close()
            Return
        End If

        If Not Enabled Then
            Close()
            Return
        End If

        If Not forcedOpened AndAlso Not AutoPopup Then
            Close()
            Return
        End If

        'build list
        BuildAutoCompleteList(forcedOpened)

        'show popup menu
        If VisibleItems.Count > 0 Then
            If forced AndAlso VisibleItems.Count = 1 AndAlso Host.ListView.SelectedItemIndex = 0 Then
                'do autocomplete if menu contains only one line and user press CTRL-SPACE
                OnSelecting()
                Close()
            Else
                ShowMenu()
            End If
        Else
            Close()
        End If
    End Sub

    Private Sub ShowMenu()
        If Not Host.Visible Then
            Dim args As New CancelEventArgs()
            OnOpening(args)
            If Not args.Cancel Then
                'calc screen point for popup menu
                Dim point As Point = TargetControlWrapper.TargetControl.Location
                point.Offset(2, TargetControlWrapper.TargetControl.Height + 2)
                point = TargetControlWrapper.GetPositionFromCharIndex(Fragment.Start)
                point.Offset(2, TargetControlWrapper.TargetControl.Font.Height + 2)

                Host.Show(TargetControlWrapper.TargetControl, point)
                If CaptureFocus Then
                    'ProcessKey((char) Keys.Down, Keys.None);
                    CType(Host.ListView, Control).Focus()
                End If
            End If
        Else
            CType(Host.ListView, Control).Invalidate()
        End If
    End Sub

    Public Overridable Function CanAddVisibleItem(items As List(Of AutoCompleteItem), item As AutoCompleteItem) As Boolean
        If item Is Nothing Then Return False
        If items Is Nothing Then Return True
        Return Not items.Contains(item)
    End Function
    Public Overridable Function SortList(list As List(Of AutoCompleteItem), str As String, ByRef selectedIndex As Integer) As List(Of AutoCompleteItem)
        Return list
    End Function
    Private Sub BuildAutoCompleteList(forced As Boolean)
        Dim visibleItems As New List(Of AutoCompleteItem)()

        Dim foundSelected As Boolean = False
        Dim selectedIndex As Integer = -1
        'get fragment around caret
        Dim fragment As Range = GetFragment(SearchPattern)
        Dim text As String = fragment.Text

        If sourceItems IsNot Nothing Then
            If forced OrElse (text.Length >= MinFragmentLength) Then ' && tb.Selection.Start == tb.Selection.End
                Me.Fragment = fragment
                'build popup menu
                For Each item As AutoCompleteItem In sourceItems
                    If item Is Nothing Then Continue For
                    If Me.MaxItemCount > 0 AndAlso visibleItems.Count >= Me.MaxItemCount Then Exit For

                    item.Parent = Me
                    Dim res As CompareResult = item.Compare(text)
                    If res <> CompareResult.Hidden AndAlso CanAddVisibleItem(visibleItems, item) Then
                        visibleItems.Add(item)
                    End If
                    If res = CompareResult.VisibleAndSelected AndAlso Not foundSelected Then
                        foundSelected = True
                        selectedIndex = visibleItems.Count - 1
                    End If
                Next
            End If
        End If

        Me.VisibleItems = SortList(visibleItems, text, selectedIndex)
        If foundSelected Then
            Me.SelectedItemIndex = selectedIndex
        Else
            Me.SelectedItemIndex = 0
        End If

        Host.ListView.HighlightedItemIndex = -1
        Host.CalcSize()
    End Sub

    Friend Sub OnOpening(args As CancelEventArgs)
        RaiseEvent Opening(Me, args)
    End Sub

    Private Function GetFragment(searchPattern As String) As Range
        Dim tb = TargetControlWrapper
        If tb.SelectionLength > 0 Then Return New Range(tb)

        Dim text As String = tb.Text
        Dim regex As New Regex(searchPattern)
        Dim result As New Range(tb)

        Dim startPos As Integer = tb.SelectionStart
        'go forward
        Dim i As Integer = startPos
        While i >= 0 AndAlso i < text.Length
            If Not regex.IsMatch(text(i).ToString()) Then
                Exit While
            End If
            i += 1
        End While
        result.[End] = i

        'go backward
        i = startPos
        While i > 0 AndAlso (i - 1) < text.Length
            If Not regex.IsMatch(text(i - 1).ToString()) Then
                Exit While
            End If
            i -= 1
        End While
        result.Start = i

        Return result
    End Function

    Public Sub Close()
        Host.ListView.HideToolTip()
        Host.Close()
        forcedOpened = False
    End Sub

    Public Sub SetAutoCompleteItems(items As IEnumerable(Of String))
        Dim list As New List(Of AutoCompleteItem)()
        If items Is Nothing Then
            sourceItems = Nothing
            Return
        End If
        For Each item As String In items
            list.Add(New AutoCompleteItem(item))
        Next
        SetAutoCompleteItems(list)
    End Sub
    Public Sub SetAutoCompleteItems(items As IEnumerable(Of AutoCompleteItem))
        sourceItems = items
    End Sub
    Public Function getAutoComplateItems() As IEnumerable(Of AutoCompleteItem)
        If sourceItems Is Nothing Then
            sourceItems = New List(Of AutoCompleteItem)()
        End If

        Return sourceItems
    End Function

    Public Overridable Function CanAddItem(items As IEnumerable(Of AutoCompleteItem), item As AutoCompleteItem) As Boolean
        If item Is Nothing Then Return False
        If items Is Nothing Then Return True
        Return Not items.Contains(item)
    End Function
    Public Sub AddItem(item As String)
        AddItem(New AutoCompleteItem(item))
    End Sub
    Public Sub AddItem(item As AutoCompleteItem)
        If sourceItems Is Nothing Then
            sourceItems = New List(Of AutoCompleteItem)()
        End If

        If TypeOf sourceItems Is IList Then
            If CanAddItem(sourceItems, item) Then CType(sourceItems, IList).Add(item)
        Else
            Throw New Exception("Current AutoComplete items does not support adding")
        End If
    End Sub
    Public Sub Clear()
        sourceItems = Nothing
    End Sub

    ''' <summary>
    ''' Shows popup menu immediately
    ''' </summary>
    ''' <param name="forced">If True - MinFragmentLength will be ignored</param>
    Public Sub Show(control As Control, forced As Boolean)
        SetAutoCompleteMenu(control, Me)
        Me.TargetControlWrapper = FindWrapper(control)
        ShowAutoComplete(forced)
    End Sub

    Friend Overridable Sub OnSelecting()
        If SelectedItemIndex < 0 OrElse SelectedItemIndex >= VisibleItems.Count Then
            Return
        End If

        Dim item As AutoCompleteItem = VisibleItems(SelectedItemIndex)
        Dim args As New SelectingEventArgs() With {
            .Item = item,
            .SelectedIndex = SelectedItemIndex
        }

        OnSelecting(args)

        If args.Cancel Then
            SelectedItemIndex = args.SelectedIndex
            CType(Host.ListView, Control).Invalidate(True)
            Return
        End If

        If Not args.Handled Then
            Dim fragment As Range = Me.Fragment
            ApplyAutoComplete(item, fragment)
        End If

        Close()

        Dim args2 As New SelectedEventArgs() With {
            .Item = item,
            .Control = TargetControlWrapper.TargetControl
        }
        item.OnSelected(args2)
        OnSelected(args2)
    End Sub

    Private Sub ApplyAutoComplete(item As AutoCompleteItem, fragment As Range)
        Dim newText As String = item.GetTextForReplace()
        'replace text of fragment
        fragment.Text = newText
        fragment.TargetWrapper.TargetControl.Focus()
    End Sub

    Friend Sub OnSelecting(args As SelectingEventArgs)
        RaiseEvent Selecting(Me, args)
    End Sub
    Public Sub OnSelected(args As SelectedEventArgs)
        RaiseEvent Selected(Me, args)
    End Sub

    Public Sub SelectNext(shift As Integer)
        SelectedItemIndex = Math.Max(0, Math.Min(SelectedItemIndex + shift, VisibleItems.Count - 1))
        CType(Host.ListView, Control).Invalidate()
    End Sub

    Public Function ProcessKey(c As Keys, keyModifiers As Keys) As Boolean
        Dim page = Host.Height / (Font.Height + 4)
        If keyModifiers = Keys.None Then
            Select Case c
                Case Keys.Down
                    SelectNext(+1)
                    Return True
                Case Keys.PageDown
                    SelectNext(+page)
                    Return True
                Case Keys.Up
                    SelectNext(-1)
                    Return True
                Case Keys.PageUp
                    SelectNext(-page)
                    Return True
                Case Keys.Enter
                    OnSelecting()
                    Return True
                Case Keys.Tab
                    If Not AllowsTabKey Then
                        Exit Select
                    End If
                    OnSelecting()
                    Return True
                Case Keys.Left, Keys.Right
                    Close()
                    Return False
                Case Keys.Escape
                    Close()
                    Return True
            End Select
        End If

        Return False
    End Function

    ''' <summary>
    ''' Menu is visible
    ''' </summary>
    Public ReadOnly Property Visible() As Boolean
        Get
            Return Host IsNot Nothing AndAlso Host.Visible
        End Get
    End Property
End Class